Add buyback invoice functionality to the existing invoice generator app, allowing users to create invoices for buying back items with price calculated per gram.
Critical: Must be backward compatible with existing production data
File: supabase/migrations/20250101000000_add_buyback_price_to_preferences.sql
-- Add buyback price per gram to user preferences
-- This is safe as it adds a new column with default value
ALTER TABLE user_preferences
ADD COLUMN buyback_price_per_gram DECIMAL(15,2) DEFAULT 0;
-- Add comment for documentation
COMMENT ON COLUMN user_preferences.buyback_price_per_gram IS 'Price per gram for buyback invoices, stored in IDR';
-- Update TypeScript types
-- File: lib/types/database.types.ts (auto-generated from Supabase)
-- The types will be regenerated after migration
Safety Check:
File: lib/types/invoice.ts
Update InvoiceItem interface to support buyback fields:
export interface InvoiceItem {
id: string;
name: string;
// Regular invoice fields
quantity?: number;
unit_price?: number;
subtotal?: number;
// Buyback invoice fields
is_buyback?: boolean;
gram?: number;
buyback_rate?: number; // Price per gram from settings
total?: number; // Auto-calculated: gram × buyback_rate
// Common fields
created_at?: string;
updated_at?: string;
}
Files to Modify:
components/features/settings/invoice-settings-tab.tsxlib/db/services/user-preferences.service.tsapp/actions/preferences.tsChanges:
buyback_price_per_gram field to InvoiceSettingsTabUI Component:
// Add to InvoiceSettingsTab component
<div className="space-y-2">
<label className="text-sm font-medium">Buyback Price per Gram</label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">
Rp
</span>
<input
type="number"
step="0.01"
min="0"
className="pl-8"
placeholder="0"
/>
</div>
<p className="text-xs text-muted-foreground">
Price per gram for buyback invoices
</p>
</div>
Files to Modify:
components/features/invoice/invoice-form.tsxlib/store.ts (Zustand store)lib/hooks/use-invoice-form.ts (if exists)Changes:
Form Structure:
Invoice Form
├── Customer Selector
├── Buyback Toggle (NEW)
│ ├── Regular Mode
│ │ ├── Item Name
│ │ ├── Quantity
│ │ ├── Unit Price
│ │ └── Subtotal (auto-calc)
│ └── Buyback Mode
│ ├── Item Name
│ ├── Gram (with decimal support)
│ └── Total (auto-calc: gram × buyback_rate)
├── Add Item Button
└── Invoice Preview
Files to Modify:
lib/utils/invoice-calculation.tslib/utils/currency.tsChanges:
calculateItemTotal() to handle buyback itemscalculateBuybackTotal() functionLogic:
export function calculateItemTotal(item: InvoiceItem, buybackRate: number): number {
if (item.is_buyback) {
// Buyback: gram × buyback_rate
return roundToTwoDecimals((item.gram || 0) * buybackRate);
} else {
// Regular: quantity × unit_price
return roundToTwoDecimals((item.quantity || 0) * (item.unit_price || 0));
}
}
Files to Modify:
All 8 template files in components/features/invoice/templates/
Changes:
Template Example (Simple):
{item.is_buyback ? (
<div>
<div style={{ fontSize: '14px', fontWeight: '500' }}>{item.name}</div>
<div style={{ fontSize: '12px', color: '#666' }}>
Weight: {item.gram}g
</div>
<div style={{ fontSize: '12px', color: '#666' }}>
Rate: {formatCurrency(item.buyback_rate || 0)}/gram
</div>
<div style={{ fontSize: '14px', fontWeight: '600', marginTop: '4px' }}>
Total: {formatCurrency(item.total || 0)}
</div>
</div>
) : (
// Existing regular item rendering
)}
Files to Modify:
Validation Rules:
// Regular mode
const regularItemSchema = z.object({
name: z.string().min(1),
quantity: z.number().positive(),
unit_price: z.number().nonnegative(),
});
// Buyback mode
const buybackItemSchema = z.object({
name: z.string().min(1),
gram: z.number().positive(),
is_buyback: z.literal(true),
});
// Conditional validation based on is_buyback flag
const invoiceItemSchema = z.discriminatedUnion('is_buyback', [
regularItemSchema.extend({ is_buyback: z.literal(false) }),
buybackItemSchema,
]);
Files to Modify:
lib/db/services/invoice.service.ts (if exists)Changes:
Unit Tests:
Integration Tests:
Manual Testing Checklist:
Edge Cases:
Error Messages:
Files to Update:
README.md - Add buyback feature documentationPre-deployment:
Post-deployment:
✅ User can set buyback price per gram in Settings ✅ Toggle works to switch between regular/buyback modes ✅ Auto-calculation works correctly (gram × price) ✅ All 8 templates render buyback items properly ✅ Image export generates correct buyback invoice ✅ No regression in existing invoice functionality ✅ Database migration is safe and backward compatible